wizards 0.2.25 → 0.2.27
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.
- package/apps/runtime/dist/agents/architect.js +5 -0
- package/apps/runtime/dist/agents/project-assistant.js +10 -1
- package/apps/runtime/dist/authoring/guide.js +61 -8
- package/apps/runtime/dist/connectors/builtin.js +17 -1
- package/apps/runtime/dist/connectors/index.js +43 -14
- package/apps/runtime/dist/connectors/plugin-connections.js +248 -0
- package/apps/runtime/dist/connectors/via-account.js +63 -0
- package/apps/runtime/dist/credits/estimate.js +12 -2
- package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +21 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +25 -3
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +29 -1
- package/apps/runtime/dist/engine/decide.js +82 -0
- package/apps/runtime/dist/engine/input.js +21 -5
- package/apps/runtime/dist/engine/requirements.js +80 -15
- package/apps/runtime/dist/engine/runner.js +252 -6
- package/apps/runtime/dist/engine/steps.js +206 -18
- package/apps/runtime/dist/engine/surface.js +90 -0
- package/apps/runtime/dist/mcp/tools.js +3 -2
- package/apps/runtime/dist/plugins/loader.js +22 -1
- package/apps/runtime/dist/plugins/registry.js +10 -0
- package/apps/runtime/dist/render/downloads.js +7 -0
- package/apps/runtime/dist/routes/runs.js +28 -7
- package/apps/runtime/dist/routes/studio.js +3 -1
- package/apps/runtime/dist/services/marketplace.js +134 -3
- package/apps/runtime/dist/services/relevance.js +17 -49
- package/apps/runtime/dist/services/runs.js +43 -15
- package/apps/runtime/dist/services/wizards.js +4 -2
- package/apps/runtime/dist/tools/pages.js +31 -6
- package/apps/runtime/dist/tools/plugin.js +29 -1
- package/apps/web/dist/assets/Account-DW396ig5.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-B7C0r9dF.js → ChangelogDialog-BdZ1SmEd.js} +1 -1
- package/apps/web/dist/assets/CreditsDialog-fOYdoev3.js +1 -0
- package/apps/web/dist/assets/EditorPage-CAARFsaI.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-D0m5Qk6S.js → FlowPreview-U2AEnKlX.js} +1 -1
- package/apps/web/dist/assets/{Harness-bIL3pHB_.js → Harness-CtDlm_2b.js} +2 -2
- package/apps/web/dist/assets/HomePage-Bd-mkLxW.js +1 -0
- package/apps/web/dist/assets/ImportWizard-BMPGjd_t.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-DjSNCRnV.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-DZPcDR9i.js +2 -0
- package/apps/web/dist/assets/ProjectPage-_LAtG3tn.js +2 -0
- package/apps/web/dist/assets/PublicRunner-6Iqp0DmU.js +1 -0
- package/apps/web/dist/assets/RunDrawer-CtQoY3Ha.js +4 -0
- package/apps/web/dist/assets/RunnerView-BLOCNZaZ.js +2 -0
- package/apps/web/dist/assets/SettingsPage-Z6skBf6I.js +1 -0
- package/apps/web/dist/assets/SetupPage-4-Lic4pT.js +1 -0
- package/apps/web/dist/assets/SharePage-DfdhXxZm.js +1 -0
- package/apps/web/dist/assets/arrow-right-BOtqnsA1.js +1 -0
- package/apps/web/dist/assets/brand-CCAI9p0f.js +2 -0
- package/apps/web/dist/assets/changelog-DQM0oz9S.js +1 -0
- package/apps/web/dist/assets/circle-alert-BC6xmJ9r.js +1 -0
- package/apps/web/dist/assets/definition-D5V79jHA.js +1 -0
- package/apps/web/dist/assets/esm-FEWi61F_.js +7 -0
- package/apps/web/dist/assets/external-link-wKL1HE6G.js +1 -0
- package/apps/web/dist/assets/image-BMdSChOR.js +1 -0
- package/apps/web/dist/assets/index-BOp2fz-2.css +1 -0
- package/apps/web/dist/assets/index-YJMooz0J.js +132 -0
- package/apps/web/dist/assets/lock-BpqzLMuv.js +1 -0
- package/apps/web/dist/assets/meta-BqZ7SlZz.js +1 -0
- package/apps/web/dist/assets/pen-line-DM-u0dES.js +1 -0
- package/apps/web/dist/assets/refresh-cw-Bk7HkOIM.js +1 -0
- package/apps/web/dist/assets/settings-menu-BM_NfZfn.js +1 -0
- package/apps/web/dist/assets/star-JHzYYl5R.js +1 -0
- package/apps/web/dist/assets/trash-CdLg-Ela.js +1 -0
- package/apps/web/dist/assets/ui-8QmvuC9u.js +9 -0
- package/apps/web/dist/assets/upload-CL1R6shM.js +1 -0
- package/apps/web/dist/assets/video-BioLmXNO.js +1 -0
- package/apps/web/dist/assets/volume-2-_IOfPNCv.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-DYtDQSJZ.js +1 -0
- package/apps/web/dist/index.html +4 -5
- package/apps/web/dist/mcp/flow.html +61 -61
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +85 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +23 -0
- package/node_modules/@engenty-wizards/shared/dist/decision.d.ts +76 -0
- package/node_modules/@engenty-wizards/shared/dist/decision.js +147 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1076 -51
- package/node_modules/@engenty-wizards/shared/dist/definition.js +597 -44
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +19 -2
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +6 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +55 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +89 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.js +198 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/Account-CpxsROpe.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-Com2Y3g8.js +0 -1
- package/apps/web/dist/assets/EditorPage-BCagWjrz.js +0 -2
- package/apps/web/dist/assets/HomePage-DPE_Z9HB.js +0 -1
- package/apps/web/dist/assets/ImportWizard-Dz2uKznp.js +0 -1
- package/apps/web/dist/assets/LocalRuntime-wMKUVZOu.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-BOxU2jWq.js +0 -2
- package/apps/web/dist/assets/ProjectPage-BJp2wqIk.js +0 -5
- package/apps/web/dist/assets/PublicRunner-BsrqVK0J.js +0 -1
- package/apps/web/dist/assets/RunDrawer-CLfJ0W_V.js +0 -1
- package/apps/web/dist/assets/RunnerView-BXMELFC4.js +0 -70
- package/apps/web/dist/assets/SettingsPage-BkuUm-bf.js +0 -1
- package/apps/web/dist/assets/SetupPage-CoyXpcvS.js +0 -1
- package/apps/web/dist/assets/SharePage-BuR-he1L.js +0 -1
- package/apps/web/dist/assets/api-rJBQEu1G.js +0 -10
- package/apps/web/dist/assets/arrow-right-6FyN3e7D.js +0 -1
- package/apps/web/dist/assets/changelog-Bmw6dg-T.js +0 -1
- package/apps/web/dist/assets/check-CUpby_J9.js +0 -1
- package/apps/web/dist/assets/chevron-down-DymW-2BP.js +0 -1
- package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +0 -1
- package/apps/web/dist/assets/createLucideIcon-q3MZMmnf.js +0 -1
- package/apps/web/dist/assets/definition-BznNdlhf.js +0 -55
- package/apps/web/dist/assets/esm-BrGYYg2h.js +0 -7
- package/apps/web/dist/assets/external-link-MAL7vJYj.js +0 -1
- package/apps/web/dist/assets/image-CC_2mJd6.js +0 -1
- package/apps/web/dist/assets/index-BZs1IxAk.css +0 -1
- package/apps/web/dist/assets/index-Co5QOA9_.js +0 -10
- package/apps/web/dist/assets/lock-DZL2N67v.js +0 -1
- package/apps/web/dist/assets/meta-B3jzXP27.js +0 -1
- package/apps/web/dist/assets/pen-line-NK46C_nG.js +0 -1
- package/apps/web/dist/assets/plus-Du-Jx8aI.js +0 -1
- package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +0 -1
- package/apps/web/dist/assets/settings-menu-CCNUb0Fd.js +0 -1
- package/apps/web/dist/assets/table-2-BdDuBcF2.js +0 -1
- package/apps/web/dist/assets/trash-0IafyhZB.js +0 -1
- package/apps/web/dist/assets/ui-DJOZSSKo.js +0 -1
- package/apps/web/dist/assets/upload-DNRENWzQ.js +0 -1
- package/apps/web/dist/assets/video-9fDPRPCv.js +0 -1
- package/apps/web/dist/assets/volume-2-DE67_rue.js +0 -1
- package/apps/web/dist/assets/wand-sparkles-Clj3VRB5.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { tableColumnsSchema } from "./engenty/data-tables/index.js";
|
|
3
3
|
import { CONNECTION_KINDS } from "./store.js";
|
|
4
|
+
import { surfaceComponentsSchema, validateSurface } from "./surface.js";
|
|
4
5
|
/** The engenty avatars a wizard can wear. Same cast as engenty's ui-core. */
|
|
5
6
|
export const ENGENTY_KINDS = [
|
|
6
7
|
"round",
|
|
@@ -33,6 +34,7 @@ export const FIELD_KINDS = [
|
|
|
33
34
|
"location",
|
|
34
35
|
"audio",
|
|
35
36
|
"signature",
|
|
37
|
+
"slot",
|
|
36
38
|
];
|
|
37
39
|
export function isLocationValue(v) {
|
|
38
40
|
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
@@ -117,6 +119,27 @@ export const itemColumnSchema = z.object({
|
|
|
117
119
|
label: z.string().min(1),
|
|
118
120
|
kind: z.enum(["text", "number", "money"]),
|
|
119
121
|
});
|
|
122
|
+
export const CONDITION_OPS = [
|
|
123
|
+
"equals",
|
|
124
|
+
"notEquals",
|
|
125
|
+
"in",
|
|
126
|
+
"notEmpty",
|
|
127
|
+
"empty",
|
|
128
|
+
"gt",
|
|
129
|
+
"lt",
|
|
130
|
+
"contains",
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* One test on what the run knows: an answer by field id, an output of an agent step as
|
|
134
|
+
* "steps.<step>.<field>", or how many rows a stored list has as "lists.<list>.count".
|
|
135
|
+
*/
|
|
136
|
+
export const conditionSchema = z.object({
|
|
137
|
+
field: z.string(),
|
|
138
|
+
op: z.enum(CONDITION_OPS),
|
|
139
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional(),
|
|
140
|
+
});
|
|
141
|
+
/** One condition, or several that must all hold. */
|
|
142
|
+
export const whenSchema = z.union([conditionSchema, z.array(conditionSchema).min(1)]);
|
|
120
143
|
export const fieldSchema = z.object({
|
|
121
144
|
id,
|
|
122
145
|
label: z.string().min(1),
|
|
@@ -124,7 +147,10 @@ export const fieldSchema = z.object({
|
|
|
124
147
|
required: z.boolean().optional(),
|
|
125
148
|
placeholder: z.string().optional(),
|
|
126
149
|
help: z.string().optional(),
|
|
127
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Choices for select / multiselect. slot: the appointment times offered, as ISO 8601 date-times
|
|
152
|
+
* with their offset ("2026-10-12T09:00:00+02:00").
|
|
153
|
+
*/
|
|
128
154
|
options: z.array(z.string()).optional(),
|
|
129
155
|
default: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional(),
|
|
130
156
|
/** Line-item columns for `items`. A row's amount is the product of its number and money columns. */
|
|
@@ -157,15 +183,54 @@ export const fieldSchema = z.object({
|
|
|
157
183
|
* `items` field takes a table whose columns are its column ids; the person corrects it.
|
|
158
184
|
*/
|
|
159
185
|
prefill: z.string().optional(),
|
|
186
|
+
/**
|
|
187
|
+
* The field is shown only while this holds. It reads an answer of this page (decided as the
|
|
188
|
+
* person types) or anything known before the page. A hidden field is not asked and not kept.
|
|
189
|
+
*/
|
|
190
|
+
when: whenSchema.optional(),
|
|
191
|
+
/**
|
|
192
|
+
* select / multiselect / slot: the choices come from earlier data — a list output
|
|
193
|
+
* ("steps.<id>.<key>"), a column of a table output ("steps.<id>.<key>.<column>") or a column
|
|
194
|
+
* of a stored list ("lists.<id>.<column>"). `options` stand in while that data is empty.
|
|
195
|
+
*/
|
|
196
|
+
optionsFrom: z.string().optional(),
|
|
197
|
+
/**
|
|
198
|
+
* The field is shown when a decision, made as the run reaches the page, finds this statement
|
|
199
|
+
* true of the run ("The complaint is about a damaged article").
|
|
200
|
+
*/
|
|
201
|
+
ask: z.string().min(1).optional(),
|
|
202
|
+
/** The id of one of the page's `groups`: of a group's fields a decision shows one (or none). */
|
|
203
|
+
group: z.string().optional(),
|
|
204
|
+
});
|
|
205
|
+
export const fieldGroupSchema = z.object({
|
|
206
|
+
id,
|
|
207
|
+
/** What the decision picks: "How should we reach the person?" */
|
|
208
|
+
instructions: z.string().min(1),
|
|
209
|
+
/** True when showing none of the group's fields is a valid answer. */
|
|
210
|
+
optional: z.boolean().optional(),
|
|
160
211
|
});
|
|
161
|
-
export const nextRuleSchema = z
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
212
|
+
export const nextRuleSchema = z
|
|
213
|
+
.object({
|
|
214
|
+
/** The rule holds when this holds … */
|
|
215
|
+
when: whenSchema.optional(),
|
|
216
|
+
/**
|
|
217
|
+
* … or, instead, when a decision finds this statement true of the run ("The person wants a
|
|
218
|
+
* refund"). All `ask` rules of a step are decided together, after no `when` rule matched.
|
|
219
|
+
*/
|
|
220
|
+
ask: z.string().min(1).optional(),
|
|
221
|
+
/**
|
|
222
|
+
* A step id, or "end". A rule with neither `when` nor `ask` is the step's "otherwise": where
|
|
223
|
+
* the run goes when no other rule holds, in place of the next step in the list.
|
|
224
|
+
*/
|
|
168
225
|
goto: z.string(),
|
|
226
|
+
/**
|
|
227
|
+
* How often this rule may send the run on in one run; afterwards it is skipped. Meant for a
|
|
228
|
+
* branch back to an earlier step (a loop), which without it is taken at most ten times.
|
|
229
|
+
*/
|
|
230
|
+
max: z.number().int().min(1).max(50).optional(),
|
|
231
|
+
})
|
|
232
|
+
.refine((rule) => rule.when === undefined || rule.ask === undefined, {
|
|
233
|
+
message: 'A branch has "when" or "ask", not both.',
|
|
169
234
|
});
|
|
170
235
|
const stepBase = {
|
|
171
236
|
id,
|
|
@@ -176,22 +241,25 @@ const stepBase = {
|
|
|
176
241
|
export const outputFieldSchema = z
|
|
177
242
|
.object({
|
|
178
243
|
id,
|
|
179
|
-
kind: z.enum(["text", "number", "list", "table", "yesno", "choice"]),
|
|
180
|
-
/** What the field holds; for `yesno` and `
|
|
244
|
+
kind: z.enum(["text", "number", "list", "table", "yesno", "choice", "score"]),
|
|
245
|
+
/** What the field holds; for `yesno`, `choice` and `score` the question it answers. */
|
|
181
246
|
description: z.string().optional(),
|
|
182
247
|
/** Table only: the exact column keys every row has — what a widget reads. */
|
|
183
248
|
columns: z.array(z.string().min(1)).optional(),
|
|
184
|
-
/** Choice
|
|
249
|
+
/** Choice: the options, one of which is the answer. Score: the levels, lowest first. */
|
|
185
250
|
options: z.array(z.string().min(1)).min(2).max(40).optional(),
|
|
186
251
|
})
|
|
187
|
-
.refine((field) => field.kind !== "choice" || Boolean(field.options?.length), {
|
|
188
|
-
|
|
252
|
+
.refine((field) => (field.kind !== "choice" && field.kind !== "score") || Boolean(field.options?.length), { message: "A choice or score field lists its options.", path: ["options"] })
|
|
253
|
+
.refine((field) => field.kind !== "score" || (field.options?.length ?? 0) <= 10, {
|
|
254
|
+
message: "A score has at most ten levels.",
|
|
189
255
|
path: ["options"],
|
|
190
256
|
});
|
|
191
257
|
export const pageStepSchema = z.object({
|
|
192
258
|
...stepBase,
|
|
193
259
|
type: z.literal("page"),
|
|
194
260
|
fields: z.array(fieldSchema).min(1),
|
|
261
|
+
/** Fields of which a decision shows one: see `group` on a field. */
|
|
262
|
+
groups: z.array(fieldGroupSchema).max(6).optional(),
|
|
195
263
|
cta: z.string().optional(),
|
|
196
264
|
});
|
|
197
265
|
export const agentStepSchema = z.object({
|
|
@@ -213,12 +281,24 @@ export const agentStepSchema = z.object({
|
|
|
213
281
|
.object({
|
|
214
282
|
format: z.enum(["text", "markdown", "json"]),
|
|
215
283
|
fields: z.array(outputFieldSchema).optional(),
|
|
284
|
+
/**
|
|
285
|
+
* json only: the step also composes a view of its result from the surface catalog, bound
|
|
286
|
+
* to its own fields; reviews and the result show that view instead of a plain table.
|
|
287
|
+
*/
|
|
288
|
+
surface: z.boolean().optional(),
|
|
216
289
|
})
|
|
217
290
|
.default({ format: "markdown" }),
|
|
218
291
|
/** The kind of model the step needs; default `high`. The model itself is bound elsewhere. */
|
|
219
292
|
model: z.enum(TEXT_CLASSES).optional(),
|
|
220
293
|
effort: z.enum(EFFORTS).optional(),
|
|
221
294
|
/** Shown to the person while it works. */
|
|
295
|
+
/**
|
|
296
|
+
* One run of the step per entry, at most twenty: the rows of a table output
|
|
297
|
+
* ("steps.<id>.<key>"), the entries of a list output, or the rows of a stored list
|
|
298
|
+
* ("lists.<id>"). The instructions read the entry as {{item.<column>}}, {{index}}, {{count}}.
|
|
299
|
+
* The output is a table: the entry's columns and the step's output fields, one row per entry.
|
|
300
|
+
*/
|
|
301
|
+
each: z.string().optional(),
|
|
222
302
|
working: z.string().optional(),
|
|
223
303
|
});
|
|
224
304
|
export const generateStepSchema = z.object({
|
|
@@ -319,6 +399,48 @@ export const filmStepSchema = z.object({
|
|
|
319
399
|
effort: z.enum(EFFORTS).optional(),
|
|
320
400
|
working: z.string().optional(),
|
|
321
401
|
});
|
|
402
|
+
/** A piece a decided surface may show: its components (the first has the piece's id) and what it shows. */
|
|
403
|
+
export const surfaceCandidateSchema = z.object({
|
|
404
|
+
id,
|
|
405
|
+
/** What this piece shows, for the decision: "A chart of prices per supplier". */
|
|
406
|
+
description: z.string().min(1),
|
|
407
|
+
components: surfaceComponentsSchema,
|
|
408
|
+
/** Pieces of a group exclude each other: the decision shows one (or none, if optional). */
|
|
409
|
+
group: z.string().optional(),
|
|
410
|
+
/** Always shown: a title, the main list. */
|
|
411
|
+
required: z.boolean().optional(),
|
|
412
|
+
});
|
|
413
|
+
export const surfaceStepSchema = z
|
|
414
|
+
.object({
|
|
415
|
+
...stepBase,
|
|
416
|
+
type: z.literal("surface"),
|
|
417
|
+
/** A view from the surface catalog, written once like a widget; every run brings new data. */
|
|
418
|
+
components: surfaceComponentsSchema.optional(),
|
|
419
|
+
/**
|
|
420
|
+
* Instead of `components`: pieces a decision picks from per run (no chart when there is
|
|
421
|
+
* nothing to chart), drawn in this order under one column.
|
|
422
|
+
*/
|
|
423
|
+
candidates: z.array(surfaceCandidateSchema).max(16).optional(),
|
|
424
|
+
groups: z.array(fieldGroupSchema).max(6).optional(),
|
|
425
|
+
/** What the surface reads as data: key → a field id, steps.id, steps.id.key, lists.id, brand.name or today. */
|
|
426
|
+
data: z.record(z.string(), z.string()).default({}),
|
|
427
|
+
working: z.string().optional(),
|
|
428
|
+
})
|
|
429
|
+
.refine((step) => Boolean(step.components) !== Boolean(step.candidates), {
|
|
430
|
+
message: 'A surface step has either "components" or "candidates".',
|
|
431
|
+
});
|
|
432
|
+
/**
|
|
433
|
+
* The components of a decided surface for the pieces kept: one root column holding them in
|
|
434
|
+
* their order. Every piece kept is the same as all of them for the validator.
|
|
435
|
+
*/
|
|
436
|
+
export function assembleSurface(candidates, kept) {
|
|
437
|
+
const keep = new Set(kept);
|
|
438
|
+
const chosen = candidates.filter((c) => keep.has(c.id));
|
|
439
|
+
return [
|
|
440
|
+
{ id: "root", component: "Column", children: chosen.map((c) => c.components[0]?.id ?? c.id) },
|
|
441
|
+
...chosen.flatMap((c) => c.components),
|
|
442
|
+
];
|
|
443
|
+
}
|
|
322
444
|
export const reviewStepSchema = z.object({
|
|
323
445
|
...stepBase,
|
|
324
446
|
type: z.literal("review"),
|
|
@@ -343,6 +465,7 @@ export const stepSchema = z.discriminatedUnion("type", [
|
|
|
343
465
|
agentStepSchema,
|
|
344
466
|
generateStepSchema,
|
|
345
467
|
widgetStepSchema,
|
|
468
|
+
surfaceStepSchema,
|
|
346
469
|
filmStepSchema,
|
|
347
470
|
reviewStepSchema,
|
|
348
471
|
resultStepSchema,
|
|
@@ -357,7 +480,8 @@ export function isDecisionStep(step) {
|
|
|
357
480
|
return (step.model === "classifier" &&
|
|
358
481
|
step.output.format === "json" &&
|
|
359
482
|
fields.length > 0 &&
|
|
360
|
-
fields.every((f) => f.kind === "yesno" || f.kind === "choice") &&
|
|
483
|
+
fields.every((f) => f.kind === "yesno" || f.kind === "choice" || f.kind === "score") &&
|
|
484
|
+
!step.each &&
|
|
361
485
|
step.tools.length === 0 &&
|
|
362
486
|
!step.mcp?.length &&
|
|
363
487
|
!step.connections?.length);
|
|
@@ -444,6 +568,9 @@ export function formatsFor(step) {
|
|
|
444
568
|
if (step.type === "widget") {
|
|
445
569
|
return step.video ? ["mp4", "png"] : ["html", "png", "pdf", "mp4", "json"];
|
|
446
570
|
}
|
|
571
|
+
if (step.type === "surface") {
|
|
572
|
+
return ["json"];
|
|
573
|
+
}
|
|
447
574
|
if (step.type === "agent") {
|
|
448
575
|
if (step.output.format === "json") {
|
|
449
576
|
return ["json", "csv", "xlsx", "md", "zip"];
|
|
@@ -536,9 +663,20 @@ export function validateWizard(def, files) {
|
|
|
536
663
|
issues.push({ stepId: step.id, message: `Duplicate field id "${field.id}".` });
|
|
537
664
|
}
|
|
538
665
|
fieldIds.add(field.id);
|
|
539
|
-
if ((field.kind === "select" || field.kind === "multiselect"
|
|
666
|
+
if ((field.kind === "select" || field.kind === "multiselect" || field.kind === "slot") &&
|
|
667
|
+
!field.options?.length &&
|
|
668
|
+
!field.optionsFrom) {
|
|
540
669
|
issues.push({ stepId: step.id, message: `Field "${field.id}" needs options.` });
|
|
541
670
|
}
|
|
671
|
+
if (field.kind === "slot") {
|
|
672
|
+
const wrong = (field.options ?? []).find((o) => !slotTime(o));
|
|
673
|
+
if (wrong !== undefined) {
|
|
674
|
+
issues.push({
|
|
675
|
+
stepId: step.id,
|
|
676
|
+
message: `Field "${field.id}": "${wrong}" is not a date and time with its offset, like "2026-10-12T09:00:00+02:00".`,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
542
680
|
if (field.kind === "items" && !field.columns?.length) {
|
|
543
681
|
issues.push({ stepId: step.id, message: `Field "${field.id}" needs columns.` });
|
|
544
682
|
}
|
|
@@ -614,10 +752,10 @@ export function validateWizard(def, files) {
|
|
|
614
752
|
// provided by the runner
|
|
615
753
|
}
|
|
616
754
|
else if (head === "item" || head === "index" || head === "count") {
|
|
617
|
-
if (step.type !== "generate" || !step.each) {
|
|
755
|
+
if ((step.type !== "generate" && step.type !== "agent") || !step.each) {
|
|
618
756
|
issues.push({
|
|
619
757
|
stepId: step.id,
|
|
620
|
-
message: `"{{${ref}}}" only exists in a generate step with "each".`,
|
|
758
|
+
message: `"{{${ref}}}" only exists in a generate or agent step with "each".`,
|
|
621
759
|
});
|
|
622
760
|
}
|
|
623
761
|
}
|
|
@@ -634,10 +772,134 @@ export function validateWizard(def, files) {
|
|
|
634
772
|
}
|
|
635
773
|
}
|
|
636
774
|
};
|
|
775
|
+
/**
|
|
776
|
+
* What a condition may read before `step`: an answer asked earlier, an output field of an
|
|
777
|
+
* earlier agent step ("steps.<step>.<field>", the step itself for a branch), or how many rows
|
|
778
|
+
* a stored list has ("lists.<list>.count").
|
|
779
|
+
*/
|
|
780
|
+
const knownBefore = (step, ref, self = false) => {
|
|
781
|
+
if (seenFields.has(ref)) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
const [head, from, key, ...more] = ref.split(".");
|
|
785
|
+
if (head === "lists") {
|
|
786
|
+
return key === "count" && more.length === 0 && listIds.has(from ?? "");
|
|
787
|
+
}
|
|
788
|
+
// `steps.<step>.<field>.p`: how probable the decision's answer is.
|
|
789
|
+
const probability = more.length === 1 && more[0] === "p";
|
|
790
|
+
if (head !== "steps" || !key || (more.length > 0 && !probability)) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
if (!(seenSteps.has(from) || (self && from === step.id))) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
const source = def.steps.find((s) => s.id === from);
|
|
797
|
+
return (source?.type === "agent" &&
|
|
798
|
+
(!probability || isDecisionStep(source)) &&
|
|
799
|
+
((source.output.fields ?? []).some((f) => f.id === key) ||
|
|
800
|
+
(Boolean(source.each) && key === "rows" && !probability)));
|
|
801
|
+
};
|
|
802
|
+
const conditionIssues = (stepId, c) => {
|
|
803
|
+
if ((c.op === "gt" || c.op === "lt") && typeof c.value !== "number") {
|
|
804
|
+
return [{ stepId, message: `"${c.op}" on "${c.field}" compares with a number value.` }];
|
|
805
|
+
}
|
|
806
|
+
if (c.op === "in" && !Array.isArray(c.value)) {
|
|
807
|
+
return [{ stepId, message: `"in" on "${c.field}" needs a list of values.` }];
|
|
808
|
+
}
|
|
809
|
+
if (c.op !== "empty" && c.op !== "notEmpty" && c.value === undefined) {
|
|
810
|
+
return [{ stepId, message: `"${c.op}" on "${c.field}" needs a value.` }];
|
|
811
|
+
}
|
|
812
|
+
return [];
|
|
813
|
+
};
|
|
814
|
+
/** Why an `optionsFrom` reference finds nothing, or null when it is fine. */
|
|
815
|
+
const optionsFromProblem = (raw) => {
|
|
816
|
+
const [head, from, key, column, ...more] = dataRef(raw).split(".");
|
|
817
|
+
if (head === "lists" && from && key && !column) {
|
|
818
|
+
const list = def.lists?.find((l) => l.id === from);
|
|
819
|
+
if (!list) {
|
|
820
|
+
return `"optionsFrom" names an unknown list "${from}".`;
|
|
821
|
+
}
|
|
822
|
+
if (sharedLists.has(from)) {
|
|
823
|
+
return notShown(from);
|
|
824
|
+
}
|
|
825
|
+
return list.columns.some((c) => c.id === key)
|
|
826
|
+
? null
|
|
827
|
+
: `"optionsFrom": list "${from}" has no column "${key}".`;
|
|
828
|
+
}
|
|
829
|
+
if (head === "steps" && from && key && more.length === 0) {
|
|
830
|
+
const source = seenSteps.has(from) ? def.steps.find((s) => s.id === from) : undefined;
|
|
831
|
+
// An agent step with `each` hands on one table, `rows`: the entry's columns and its fields.
|
|
832
|
+
if (source?.type === "agent" && source.each && key === "rows") {
|
|
833
|
+
return column ? null : `"optionsFrom": "${from}.rows" is a table; name one of its columns.`;
|
|
834
|
+
}
|
|
835
|
+
const out = source?.type === "agent"
|
|
836
|
+
? (source.output.fields ?? []).find((f) => f.id === key)
|
|
837
|
+
: undefined;
|
|
838
|
+
if (!out) {
|
|
839
|
+
return `"optionsFrom" names "${from}.${key}", which is not an output of an earlier agent step.`;
|
|
840
|
+
}
|
|
841
|
+
if (column) {
|
|
842
|
+
return out.kind === "table" && (!out.columns?.length || out.columns.includes(column))
|
|
843
|
+
? null
|
|
844
|
+
: `"optionsFrom": "${from}.${key}" is not a table with the column "${column}".`;
|
|
845
|
+
}
|
|
846
|
+
return out.kind === "list" ? null : `"optionsFrom": "${from}.${key}" is not a list.`;
|
|
847
|
+
}
|
|
848
|
+
return `"optionsFrom" is "steps.<id>.<key>", "steps.<id>.<key>.<column>" or "lists.<id>.<column>".`;
|
|
849
|
+
};
|
|
637
850
|
for (const step of def.steps) {
|
|
638
851
|
switch (step.type) {
|
|
639
|
-
case "page":
|
|
852
|
+
case "page": {
|
|
853
|
+
const groups = new Set();
|
|
854
|
+
for (const g of step.groups ?? []) {
|
|
855
|
+
if (groups.has(g.id)) {
|
|
856
|
+
issues.push({ stepId: step.id, message: `Duplicate field group "${g.id}".` });
|
|
857
|
+
}
|
|
858
|
+
groups.add(g.id);
|
|
859
|
+
if (!step.fields.some((f) => f.group === g.id)) {
|
|
860
|
+
issues.push({ stepId: step.id, message: `Field group "${g.id}" has no fields.` });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const decided = step.fields.some((f) => f.ask || f.group);
|
|
864
|
+
if (decided && step.fields.length > MAX_DECIDED_FIELDS) {
|
|
865
|
+
issues.push({
|
|
866
|
+
stepId: step.id,
|
|
867
|
+
message: `A page whose fields are decided has at most ${MAX_DECIDED_FIELDS} fields.`,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
640
870
|
for (const field of step.fields) {
|
|
871
|
+
if (field.ask && field.group) {
|
|
872
|
+
issues.push({
|
|
873
|
+
stepId: step.id,
|
|
874
|
+
message: `Field "${field.id}" has "ask" or "group", not both.`,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (field.group && !groups.has(field.group)) {
|
|
878
|
+
issues.push({
|
|
879
|
+
stepId: step.id,
|
|
880
|
+
message: `Field "${field.id}" names an unknown group "${field.group}".`,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const field of step.fields) {
|
|
885
|
+
for (const c of conditionsOf(field.when)) {
|
|
886
|
+
const own = step.fields.some((f) => f.id === c.field && f.id !== field.id);
|
|
887
|
+
if (!own && !knownBefore(step, c.field)) {
|
|
888
|
+
issues.push({
|
|
889
|
+
stepId: step.id,
|
|
890
|
+
message: `Field "${field.id}": "when" reads "${c.field}", which is neither on this page nor known before it.`,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
issues.push(...conditionIssues(step.id, c));
|
|
894
|
+
}
|
|
895
|
+
if (field.optionsFrom) {
|
|
896
|
+
const problem = field.kind !== "select" && field.kind !== "multiselect" && field.kind !== "slot"
|
|
897
|
+
? `"optionsFrom" fills the choices of a select, multiselect or slot; kind is "${field.kind}".`
|
|
898
|
+
: optionsFromProblem(field.optionsFrom);
|
|
899
|
+
if (problem) {
|
|
900
|
+
issues.push({ stepId: step.id, message: `Field "${field.id}": ${problem}` });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
641
903
|
if (field.prefill) {
|
|
642
904
|
const ref = dataRef(field.prefill);
|
|
643
905
|
if (ref.split(".")[0] !== "steps") {
|
|
@@ -653,6 +915,7 @@ export function validateWizard(def, files) {
|
|
|
653
915
|
seenFields.add(field.id);
|
|
654
916
|
}
|
|
655
917
|
break;
|
|
918
|
+
}
|
|
656
919
|
case "agent":
|
|
657
920
|
checkTemplate(step, step.instructions);
|
|
658
921
|
for (const c of step.connections ?? []) {
|
|
@@ -660,6 +923,30 @@ export function validateWizard(def, files) {
|
|
|
660
923
|
issues.push({ stepId: step.id, message: `Step uses unknown connection "${c}".` });
|
|
661
924
|
}
|
|
662
925
|
}
|
|
926
|
+
if (step.output.surface && step.output.format !== "json") {
|
|
927
|
+
issues.push({
|
|
928
|
+
stepId: step.id,
|
|
929
|
+
message: 'A step composes a view ("surface") of json output only.',
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (step.each) {
|
|
933
|
+
const ref = dataRef(step.each);
|
|
934
|
+
const list = ref.match(/^lists\.([a-zA-Z][a-zA-Z0-9_]*)$/)?.[1];
|
|
935
|
+
if (list) {
|
|
936
|
+
if (!listIds.has(list)) {
|
|
937
|
+
issues.push({ stepId: step.id, message: `each names an unknown list "${list}".` });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
else if (ref.startsWith("steps.") && ref.split(".").length === 3) {
|
|
941
|
+
checkTemplate(step, `{{${ref}}}`);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
issues.push({
|
|
945
|
+
stepId: step.id,
|
|
946
|
+
message: `each "${step.each}" is "steps.<id>.<key>" (a table or list output) or "lists.<id>".`,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
663
950
|
break;
|
|
664
951
|
case "generate":
|
|
665
952
|
checkTemplate(step, step.prompt);
|
|
@@ -711,6 +998,49 @@ export function validateWizard(def, files) {
|
|
|
711
998
|
});
|
|
712
999
|
}
|
|
713
1000
|
break;
|
|
1001
|
+
case "surface": {
|
|
1002
|
+
for (const [key, ref] of Object.entries(step.data)) {
|
|
1003
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1004
|
+
issues.push({
|
|
1005
|
+
stepId: step.id,
|
|
1006
|
+
message: `Surface data key "${key}" is not an identifier.`,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
const list = listRef(dataRef(ref));
|
|
1010
|
+
if (list) {
|
|
1011
|
+
if (!listIds.has(list)) {
|
|
1012
|
+
issues.push({
|
|
1013
|
+
stepId: step.id,
|
|
1014
|
+
message: `Surface data reads an unknown list "${list}".`,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
checkTemplate(step, `{{${dataRef(ref)}}}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const groups = new Set((step.groups ?? []).map((g) => g.id));
|
|
1023
|
+
for (const c of step.candidates ?? []) {
|
|
1024
|
+
if (c.components[0]?.id !== c.id) {
|
|
1025
|
+
issues.push({
|
|
1026
|
+
stepId: step.id,
|
|
1027
|
+
message: `Surface piece "${c.id}": its first component carries the piece's id.`,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (c.group && !groups.has(c.group)) {
|
|
1031
|
+
issues.push({
|
|
1032
|
+
stepId: step.id,
|
|
1033
|
+
message: `Surface piece "${c.id}" names an unknown group "${c.group}".`,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const components = step.components ??
|
|
1038
|
+
assembleSurface(step.candidates ?? [], (step.candidates ?? []).map((c) => c.id));
|
|
1039
|
+
for (const issue of validateSurface(components, Object.keys(step.data))) {
|
|
1040
|
+
issues.push({ stepId: step.id, message: `Surface: ${issue.message}` });
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
714
1044
|
case "film":
|
|
715
1045
|
checkTemplate(step, step.brief);
|
|
716
1046
|
for (const [name, ref] of Object.entries(step.inputs)) {
|
|
@@ -778,23 +1108,23 @@ export function validateWizard(def, files) {
|
|
|
778
1108
|
}
|
|
779
1109
|
break;
|
|
780
1110
|
}
|
|
1111
|
+
if ((step.next ?? []).filter(isOtherwise).length > 1) {
|
|
1112
|
+
issues.push({
|
|
1113
|
+
stepId: step.id,
|
|
1114
|
+
message: 'A step has at most one "otherwise" branch (one without "when" and "ask").',
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
781
1117
|
for (const rule of step.next ?? []) {
|
|
782
1118
|
if (rule.goto !== "end" && !stepIds.has(rule.goto)) {
|
|
783
1119
|
issues.push({ stepId: step.id, message: `Branch goes to unknown step "${rule.goto}".` });
|
|
784
1120
|
}
|
|
785
|
-
// A branch reads what the person answered,
|
|
786
|
-
// agent step
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
(source?.type === "agent" && (source.output.fields ?? []).some((f) => f.id === key));
|
|
793
|
-
if (!known) {
|
|
794
|
-
issues.push({
|
|
795
|
-
stepId: step.id,
|
|
796
|
-
message: `Branch reads unknown field "${rule.when.field}".`,
|
|
797
|
-
});
|
|
1121
|
+
// A branch reads what the person answered (on any page), an output field of this or an
|
|
1122
|
+
// earlier agent step, or the row count of a stored list.
|
|
1123
|
+
for (const c of conditionsOf(rule.when)) {
|
|
1124
|
+
if (!fieldIds.has(c.field) && !knownBefore(step, c.field, true)) {
|
|
1125
|
+
issues.push({ stepId: step.id, message: `Branch reads unknown field "${c.field}".` });
|
|
1126
|
+
}
|
|
1127
|
+
issues.push(...conditionIssues(step.id, c));
|
|
798
1128
|
}
|
|
799
1129
|
}
|
|
800
1130
|
seenSteps.add(step.id);
|
|
@@ -817,48 +1147,271 @@ export function parseWizard(input, files) {
|
|
|
817
1147
|
* What branch rules can read in a run: the person's answers by field id, and the structured
|
|
818
1148
|
* output of the steps run so far as `steps.<step>.<field>`.
|
|
819
1149
|
*/
|
|
820
|
-
export function branchValues(values, outputs
|
|
1150
|
+
export function branchValues(values, outputs,
|
|
1151
|
+
/** Rows per stored list, read as `lists.<list>.count`. */
|
|
1152
|
+
listCounts = {}) {
|
|
821
1153
|
const out = { ...values };
|
|
1154
|
+
for (const [list, count] of Object.entries(listCounts)) {
|
|
1155
|
+
out[`lists.${list}.count`] = count;
|
|
1156
|
+
}
|
|
822
1157
|
for (const [stepId, output] of Object.entries(outputs)) {
|
|
823
1158
|
if (output.json && typeof output.json === "object" && !Array.isArray(output.json)) {
|
|
824
1159
|
for (const [key, value] of Object.entries(output.json)) {
|
|
825
1160
|
out[`steps.${stepId}.${key}`] = value;
|
|
826
1161
|
}
|
|
827
1162
|
}
|
|
1163
|
+
for (const [key, p] of Object.entries(output.decided ?? {})) {
|
|
1164
|
+
out[`steps.${stepId}.${key}.p`] = p;
|
|
1165
|
+
}
|
|
828
1166
|
}
|
|
829
1167
|
return out;
|
|
830
1168
|
}
|
|
831
|
-
/**
|
|
832
|
-
export
|
|
1169
|
+
/** A branch back to an earlier step without `max` is taken at most this often in one run. */
|
|
1170
|
+
export const DEFAULT_LOOP_MAX = 10;
|
|
1171
|
+
/** How a run counts the times it took a branch: "<step>→<goto>". */
|
|
1172
|
+
export function loopKey(stepId, goto) {
|
|
1173
|
+
return `${stepId}→${goto}`;
|
|
1174
|
+
}
|
|
1175
|
+
/** Whether a rule may still send the run on: one with `max`, or one back, stops after so many. */
|
|
1176
|
+
export function ruleOpen(def, stepId, rule, loops = {}) {
|
|
1177
|
+
const from = def.steps.findIndex((s) => s.id === stepId);
|
|
1178
|
+
const to = rule.goto === "end" ? def.steps.length : def.steps.findIndex((s) => s.id === rule.goto);
|
|
1179
|
+
const max = rule.max ?? (to >= 0 && to <= from ? DEFAULT_LOOP_MAX : Number.POSITIVE_INFINITY);
|
|
1180
|
+
return (loops[loopKey(stepId, rule.goto)] ?? 0) < max;
|
|
1181
|
+
}
|
|
1182
|
+
export function followRules(def, stepId, values, loops = {}) {
|
|
833
1183
|
const index = def.steps.findIndex((s) => s.id === stepId);
|
|
834
1184
|
const step = def.steps[index];
|
|
835
1185
|
if (!step) {
|
|
836
|
-
return null;
|
|
1186
|
+
return { kind: "go", cursor: null, rule: null };
|
|
837
1187
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1188
|
+
const rules = step.next ?? [];
|
|
1189
|
+
for (const [i, rule] of rules.entries()) {
|
|
1190
|
+
if (ruleOpen(def, stepId, rule, loops) && ruleMatches(rule, values)) {
|
|
1191
|
+
return { kind: "go", cursor: rule.goto === "end" ? null : rule.goto, rule: i };
|
|
841
1192
|
}
|
|
842
1193
|
}
|
|
1194
|
+
const asks = rules.flatMap((rule, i) => rule.ask && ruleOpen(def, stepId, rule, loops) ? [i] : []);
|
|
1195
|
+
if (asks.length) {
|
|
1196
|
+
return { kind: "decide", rules: asks };
|
|
1197
|
+
}
|
|
1198
|
+
return { kind: "go", ...otherwiseOf(def, stepId, loops) };
|
|
1199
|
+
}
|
|
1200
|
+
/** A rule with neither a condition nor a statement: where the run goes when no other rule holds. */
|
|
1201
|
+
export function isOtherwise(rule) {
|
|
1202
|
+
return rule.when === undefined && rule.ask === undefined;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Where a step goes when none of its rules holds: its "otherwise" rule (`rule` is its index in
|
|
1206
|
+
* `next`), else the next step in the list. `cursor` null = the end.
|
|
1207
|
+
*/
|
|
1208
|
+
export function otherwiseOf(def, stepId, loops = {}) {
|
|
1209
|
+
const index = def.steps.findIndex((s) => s.id === stepId);
|
|
1210
|
+
const rules = def.steps[index]?.next ?? [];
|
|
1211
|
+
const i = rules.findIndex((r) => isOtherwise(r) && ruleOpen(def, stepId, r, loops));
|
|
1212
|
+
if (i >= 0) {
|
|
1213
|
+
return { cursor: rules[i].goto === "end" ? null : rules[i].goto, rule: i };
|
|
1214
|
+
}
|
|
1215
|
+
return { cursor: def.steps[index + 1]?.id ?? null, rule: null };
|
|
1216
|
+
}
|
|
1217
|
+
/** The step after `step` by its `when` rules alone. `null` = the end. */
|
|
1218
|
+
export function nextStepId(def, stepId, values) {
|
|
1219
|
+
const outcome = followRules(def, stepId, values);
|
|
1220
|
+
if (outcome.kind === "go") {
|
|
1221
|
+
return outcome.cursor;
|
|
1222
|
+
}
|
|
1223
|
+
const index = def.steps.findIndex((s) => s.id === stepId);
|
|
843
1224
|
return def.steps[index + 1]?.id ?? null;
|
|
844
1225
|
}
|
|
845
1226
|
function isEmptyValue(v) {
|
|
846
1227
|
return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
847
1228
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1229
|
+
/** The conditions of a rule or a field, as a list: one, several, or none. */
|
|
1230
|
+
export function conditionsOf(when) {
|
|
1231
|
+
return when === undefined ? [] : Array.isArray(when) ? when : [when];
|
|
1232
|
+
}
|
|
1233
|
+
/** A typed number as a number: "89,90" from a German number pad counts as 89.9. */
|
|
1234
|
+
function numberOf(v) {
|
|
1235
|
+
if (typeof v === "number") {
|
|
1236
|
+
return Number.isFinite(v) ? v : null;
|
|
1237
|
+
}
|
|
1238
|
+
if (typeof v !== "string" || !v.trim()) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
const n = Number(v.replace(/\s/g, "").replace(",", "."));
|
|
1242
|
+
return Number.isFinite(n) ? n : null;
|
|
1243
|
+
}
|
|
1244
|
+
export function conditionMatches(c, values) {
|
|
1245
|
+
const v = values[c.field];
|
|
1246
|
+
switch (c.op) {
|
|
851
1247
|
case "empty":
|
|
852
1248
|
return isEmptyValue(v);
|
|
853
1249
|
case "notEmpty":
|
|
854
1250
|
return !isEmptyValue(v);
|
|
855
1251
|
case "equals":
|
|
856
|
-
return String(v) === String(
|
|
1252
|
+
return String(v) === String(c.value);
|
|
857
1253
|
case "notEquals":
|
|
858
|
-
return String(v) !== String(
|
|
1254
|
+
return String(v) !== String(c.value);
|
|
859
1255
|
case "in":
|
|
860
|
-
return Array.isArray(
|
|
1256
|
+
return Array.isArray(c.value) && c.value.map(String).includes(String(v));
|
|
1257
|
+
case "gt":
|
|
1258
|
+
case "lt": {
|
|
1259
|
+
const a = numberOf(v);
|
|
1260
|
+
const b = numberOf(c.value);
|
|
1261
|
+
return a !== null && b !== null && (c.op === "gt" ? a > b : a < b);
|
|
1262
|
+
}
|
|
1263
|
+
case "contains":
|
|
1264
|
+
if (Array.isArray(v)) {
|
|
1265
|
+
return v.map(String).includes(String(c.value));
|
|
1266
|
+
}
|
|
1267
|
+
return (typeof v === "string" &&
|
|
1268
|
+
c.value !== undefined &&
|
|
1269
|
+
v.toLowerCase().includes(String(c.value).toLowerCase()));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/** Whether every condition holds; no conditions hold trivially. */
|
|
1273
|
+
export function whenHolds(when, values) {
|
|
1274
|
+
return conditionsOf(when).every((c) => conditionMatches(c, values));
|
|
1275
|
+
}
|
|
1276
|
+
/** Whether a `when` rule holds. An `ask` rule never holds here: a decision takes it (see decide). */
|
|
1277
|
+
export function ruleMatches(rule, values) {
|
|
1278
|
+
return rule.when !== undefined && whenHolds(rule.when, values);
|
|
1279
|
+
}
|
|
1280
|
+
/** A page with decided fields has at most this many; it shows at most MAX_SHOWN_FIELDS of them. */
|
|
1281
|
+
export const MAX_DECIDED_FIELDS = 12;
|
|
1282
|
+
export const MAX_SHOWN_FIELDS = 5;
|
|
1283
|
+
/** Whether a field is shown only when a decision keeps it. */
|
|
1284
|
+
export function isDecidedField(field) {
|
|
1285
|
+
return Boolean(field.ask || field.group);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* The fields of a page that are shown: those whose `when` holds on what was known before the
|
|
1289
|
+
* page (`known`) and what the page holds so far (`page`). A hidden field's value does not count
|
|
1290
|
+
* for the fields after it.
|
|
1291
|
+
*/
|
|
1292
|
+
export function shownFields(fields, known, page) {
|
|
1293
|
+
const values = { ...known, ...page };
|
|
1294
|
+
const out = [];
|
|
1295
|
+
for (const field of fields) {
|
|
1296
|
+
if (whenHolds(field.when, values)) {
|
|
1297
|
+
out.push(field);
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
delete values[field.id];
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
/** The references the `when` of a page's fields read outside the page: what the page must know. */
|
|
1306
|
+
export function pageNeeds(fields) {
|
|
1307
|
+
const own = new Set(fields.map((f) => f.id));
|
|
1308
|
+
return [
|
|
1309
|
+
...new Set(fields.flatMap((f) => conditionsOf(f.when).map((c) => c.field)).filter((r) => !own.has(r))),
|
|
1310
|
+
];
|
|
1311
|
+
}
|
|
1312
|
+
/** At most this many choices come from data. */
|
|
1313
|
+
export const MAX_DATA_OPTIONS = 100;
|
|
1314
|
+
/**
|
|
1315
|
+
* The choices an `optionsFrom` reference finds in a run: the entries of a list output, one
|
|
1316
|
+
* column of a table output, or one column of a stored list — as text, without duplicates and
|
|
1317
|
+
* empties. Empty when the data is not there (yet).
|
|
1318
|
+
*/
|
|
1319
|
+
export function optionsFromData(ref, outputs, lists = {}) {
|
|
1320
|
+
const [head, id, key, column, ...more] = dataRef(ref).split(".");
|
|
1321
|
+
let raw = [];
|
|
1322
|
+
if (head === "lists" && id && key && !column) {
|
|
1323
|
+
raw = (lists[id]?.rows ?? []).map((r) => r.cells[key]);
|
|
1324
|
+
}
|
|
1325
|
+
else if (head === "steps" && id && key && more.length === 0) {
|
|
1326
|
+
const json = outputs[id]?.json;
|
|
1327
|
+
const value = json && typeof json === "object" ? json[key] : undefined;
|
|
1328
|
+
if (Array.isArray(value)) {
|
|
1329
|
+
raw = column
|
|
1330
|
+
? value.map((row) => row && typeof row === "object" ? row[column] : undefined)
|
|
1331
|
+
: value;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const seen = new Set();
|
|
1335
|
+
for (const v of raw) {
|
|
1336
|
+
if (v === null || v === undefined || typeof v === "object") {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const text = String(v).trim().slice(0, 200);
|
|
1340
|
+
if (text) {
|
|
1341
|
+
seen.add(text);
|
|
1342
|
+
}
|
|
1343
|
+
if (seen.size >= MAX_DATA_OPTIONS) {
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return [...seen];
|
|
1348
|
+
}
|
|
1349
|
+
// ---------------------------------------------------------------------------
|
|
1350
|
+
// Appointment times
|
|
1351
|
+
const SLOT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
1352
|
+
/**
|
|
1353
|
+
* A slot option as day and time in its own offset (the business's time zone, not the reader's).
|
|
1354
|
+
* Null when it is not an ISO 8601 date-time with an offset.
|
|
1355
|
+
*/
|
|
1356
|
+
export function slotTime(iso) {
|
|
1357
|
+
const m = SLOT_RE.exec(iso.trim());
|
|
1358
|
+
if (!m) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
const [, y, mo, d, h, mi] = m.map(Number);
|
|
1362
|
+
const wall = new Date(Date.UTC(y, mo - 1, d, h, mi));
|
|
1363
|
+
if (wall.getUTCMonth() !== mo - 1 ||
|
|
1364
|
+
wall.getUTCDate() !== d ||
|
|
1365
|
+
h > 23 ||
|
|
1366
|
+
mi > 59 ||
|
|
1367
|
+
!Number.isFinite(Date.parse(iso))) {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
return { day: `${m[1]}-${m[2]}-${m[3]}`, time: `${m[4]}:${m[5]}`, wall };
|
|
1371
|
+
}
|
|
1372
|
+
/** The weekday and date of a slot's day: "Montag, 12. Oktober" / "Monday, 12 October". */
|
|
1373
|
+
export function slotDayText(iso, lang = "de") {
|
|
1374
|
+
const slot = slotTime(iso);
|
|
1375
|
+
if (!slot) {
|
|
1376
|
+
return iso;
|
|
1377
|
+
}
|
|
1378
|
+
const locale = LOCALES[lang];
|
|
1379
|
+
const weekday = slot.wall.toLocaleDateString(locale, { weekday: "long", timeZone: "UTC" });
|
|
1380
|
+
const date = slot.wall.toLocaleDateString(locale, {
|
|
1381
|
+
day: "numeric",
|
|
1382
|
+
month: "long",
|
|
1383
|
+
timeZone: "UTC",
|
|
1384
|
+
});
|
|
1385
|
+
return `${weekday}, ${date}`;
|
|
1386
|
+
}
|
|
1387
|
+
/** A chosen slot as a person reads it back: "Di 13.10.2026, 10:30" / "Tue 13/10/2026, 10:30". */
|
|
1388
|
+
export function slotText(iso, lang = "de") {
|
|
1389
|
+
const slot = slotTime(iso);
|
|
1390
|
+
if (!slot) {
|
|
1391
|
+
return iso;
|
|
1392
|
+
}
|
|
1393
|
+
const locale = LOCALES[lang];
|
|
1394
|
+
const weekday = slot.wall
|
|
1395
|
+
.toLocaleDateString(locale, { weekday: "short", timeZone: "UTC" })
|
|
1396
|
+
.replace(/\.$/, "");
|
|
1397
|
+
const date = slot.wall.toLocaleDateString(locale, {
|
|
1398
|
+
day: "2-digit",
|
|
1399
|
+
month: "2-digit",
|
|
1400
|
+
year: "numeric",
|
|
1401
|
+
timeZone: "UTC",
|
|
1402
|
+
});
|
|
1403
|
+
return `${weekday} ${date}, ${slot.time}`;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* The slot among `options` that is the same moment as `value`, written as offered — so an answer
|
|
1407
|
+
* in another offset ("…T07:00:00Z" for "…T09:00:00+02:00") picks the offered time.
|
|
1408
|
+
*/
|
|
1409
|
+
export function offeredSlot(value, options) {
|
|
1410
|
+
if (options.includes(value)) {
|
|
1411
|
+
return value;
|
|
861
1412
|
}
|
|
1413
|
+
const at = slotTime(value) ? Date.parse(value) : Number.NaN;
|
|
1414
|
+
return Number.isFinite(at) ? options.find((o) => slotTime(o) && Date.parse(o) === at) : undefined;
|
|
862
1415
|
}
|
|
863
1416
|
export function itemsTotals(field, value, values) {
|
|
864
1417
|
const rows = Array.isArray(value) ? value : [];
|