@marktiderman/genesis-forms 0.0.0-stage → 0.1.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/conformance/after-class-back-after-a-jump.scenario.json +217 -0
  4. package/conformance/after-class-contact-yes-completed.scenario.json +230 -0
  5. package/conformance/after-class-contact-yes-requires-name-and-email.scenario.json +218 -0
  6. package/conformance/after-class-happy-path.scenario.json +237 -0
  7. package/conformance/after-class-invalid-email-rejected.scenario.json +227 -0
  8. package/conformance/after-class-low-rating-skips-went-well.scenario.json +223 -0
  9. package/conformance/after-class-server-drops-unknown-and-hidden-answers.scenario.json +236 -0
  10. package/conformance/after-class-server-ignores-a-client-sent-coach.scenario.json +219 -0
  11. package/conformance/contact-details-anonymous-form-skips-identifying-fields.scenario.json +125 -0
  12. package/conformance/contact-details-named-form-asks-for-identifying-fields.scenario.json +129 -0
  13. package/conformance/monthly-pulse-default-ending.scenario.json +148 -0
  14. package/conformance/monthly-pulse-promoter-ending-via-jump.scenario.json +128 -0
  15. package/dist/engine-Cp5t2_Am.d.ts +75 -0
  16. package/dist/engine-LsPRTzt3.d.cts +75 -0
  17. package/dist/index.cjs +1205 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +374 -0
  20. package/dist/index.d.ts +374 -0
  21. package/dist/index.js +1174 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/server.cjs +988 -0
  24. package/dist/server.cjs.map +1 -0
  25. package/dist/server.d.cts +52 -0
  26. package/dist/server.d.ts +52 -0
  27. package/dist/server.js +984 -0
  28. package/dist/server.js.map +1 -0
  29. package/dist/testing.cjs +1299 -0
  30. package/dist/testing.cjs.map +1 -0
  31. package/dist/testing.d.cts +129 -0
  32. package/dist/testing.d.ts +129 -0
  33. package/dist/testing.js +1293 -0
  34. package/dist/testing.js.map +1 -0
  35. package/dist/validate-j8Z188mF.d.cts +175 -0
  36. package/dist/validate-j8Z188mF.d.ts +175 -0
  37. package/package.json +79 -3
@@ -0,0 +1,374 @@
1
+ import { F as FieldBlock, C as ContentBlock, P as PageBreakBlock, a as FormDefinition, H as HiddenInput, b as FormLayout, L as LogicRule, E as Ending, c as FormSettings, J as Json, d as Condition, B as Block, O as Operator, e as FieldValidator } from './validate-j8Z188mF.js';
2
+ export { f as FormEvent, g as Operand, R as REQUIRED_MESSAGE, S as StandardSchemaV1, v as validateField } from './validate-j8Z188mF.js';
3
+ import { F as FormEngine } from './engine-Cp5t2_Am.js';
4
+ export { C as CreateFormOptions, a as FormState, c as createForm, i as isFormEvent, b as isJson, r as replay } from './engine-Cp5t2_Am.js';
5
+
6
+ /** A field block that remembers its key and value type, for answer inference. */
7
+ type FieldSpec<K extends string = string, V = unknown> = FieldBlock & {
8
+ readonly key: K;
9
+ /** Type-level only. */
10
+ readonly __value?: V;
11
+ };
12
+ interface FieldOptions {
13
+ label: string;
14
+ description?: string;
15
+ placeholder?: string;
16
+ required?: boolean;
17
+ /** Personal or secret data (see `isSensitive`). */
18
+ sensitive?: boolean;
19
+ /** Identifies the respondent (name, email, phone). Not asked when `settings.anonymous` is true. */
20
+ identifying?: boolean;
21
+ /** Defaults to the key. Set it before renaming a key on a live form, so answers stay linked. */
22
+ id?: string;
23
+ }
24
+ interface ChoiceFieldOptions<O extends string> extends FieldOptions {
25
+ options: readonly {
26
+ value: O;
27
+ label?: string;
28
+ }[];
29
+ }
30
+ interface HiddenOptions {
31
+ source?: HiddenInput["source"];
32
+ sensitive?: boolean;
33
+ }
34
+ declare const f: {
35
+ shortText: <K extends string>(key: K, o: FieldOptions & {
36
+ maxLength?: number;
37
+ }) => FieldSpec<K, string>;
38
+ longText: <K extends string>(key: K, o: FieldOptions & {
39
+ maxLength?: number;
40
+ }) => FieldSpec<K, string>;
41
+ email: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, string>;
42
+ phone: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, string>;
43
+ rating: <K extends string>(key: K, o: FieldOptions & {
44
+ max?: number;
45
+ }) => FieldSpec<K, number>;
46
+ nps: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, number>;
47
+ singleChoice: <K extends string, O extends string>(key: K, o: ChoiceFieldOptions<O>) => FieldSpec<K, O>;
48
+ multiChoice: <K extends string, O extends string>(key: K, o: ChoiceFieldOptions<O>) => FieldSpec<K, O[]>;
49
+ yesNo: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, boolean>;
50
+ /** Any registered field type, including plugins. */
51
+ custom: <K extends string, V = Json>(type: string, key: K, o: FieldOptions & {
52
+ props?: Record<string, Json>;
53
+ }) => FieldSpec<K, V>;
54
+ heading: (id: string, text: string) => ContentBlock;
55
+ text: (id: string, text: string) => ContentBlock;
56
+ pageBreak: (id: string, title?: string) => PageBreakBlock;
57
+ hidden: {
58
+ string: (o?: HiddenOptions) => {
59
+ sensitive?: true | undefined;
60
+ type: "string";
61
+ source: "url" | "host" | "server";
62
+ };
63
+ number: (o?: HiddenOptions) => {
64
+ sensitive?: true | undefined;
65
+ type: "number";
66
+ source: "url" | "host" | "server";
67
+ };
68
+ boolean: (o?: HiddenOptions) => {
69
+ sensitive?: true | undefined;
70
+ type: "boolean";
71
+ source: "url" | "host" | "server";
72
+ };
73
+ };
74
+ };
75
+ /**
76
+ * Resolves a field key to its id. `"target"` (a show/hide target or a jump
77
+ * destination) also accepts the id of a content block.
78
+ */
79
+ type KeyResolver = (key: string, as?: "field" | "target") => string;
80
+ /** A rule written with field keys; `defineForm` resolves keys to ids. */
81
+ interface RuleSpec {
82
+ readonly __rule: true;
83
+ build(resolve: KeyResolver, id: string): LogicRule;
84
+ }
85
+ type Effects = {
86
+ /** Show a field (by key) or a content block (by id). */
87
+ show(targetKey: string): RuleSpec;
88
+ /** Hide a field (by key) or a content block (by id). */
89
+ hide(targetKey: string): RuleSpec;
90
+ /** Jump from this field to a later field or content block, or to an ending. */
91
+ jumpTo(target: string | {
92
+ ending: string;
93
+ }): RuleSpec;
94
+ };
95
+ /** Start a rule on a field: `when("overall").lessThan(4).jumpTo("improve")`. */
96
+ declare function when(key: string): {
97
+ is: (value: Json) => Effects;
98
+ isNot: (value: Json) => Effects;
99
+ lessThan: (n: number) => Effects;
100
+ atMost: (n: number) => Effects;
101
+ greaterThan: (n: number) => Effects;
102
+ atLeast: (n: number) => Effects;
103
+ isOneOf: (values: Json[]) => Effects;
104
+ includes: (value: Json) => Effects;
105
+ isAnswered: () => Effects;
106
+ isEmpty: () => Effects;
107
+ };
108
+ declare namespace when {
109
+ var condition: (condition: Condition, from?: string) => Effects;
110
+ }
111
+ type HiddenSpec = {
112
+ type: HiddenInput["type"];
113
+ source: HiddenInput["source"];
114
+ sensitive?: boolean;
115
+ };
116
+ type HiddenValue<T extends HiddenSpec["type"]> = T extends "number" ? number : T extends "boolean" ? boolean : string;
117
+ type AnyBlock = FieldSpec<string, unknown> | ContentBlock | PageBreakBlock;
118
+ type AnswersOf<B extends readonly AnyBlock[]> = {
119
+ [E in B[number] as E extends FieldSpec<infer K, unknown> ? K : never]?: E extends FieldSpec<string, infer V> ? V : never;
120
+ };
121
+ interface FormInput<B extends readonly AnyBlock[], H extends Record<string, HiddenSpec>> {
122
+ id: string;
123
+ title: string;
124
+ description?: string;
125
+ layout?: FormLayout;
126
+ /** Authoring sugar: a map of name → spec. Normalized to the canonical array. */
127
+ hidden?: H;
128
+ blocks: B;
129
+ logic?: RuleSpec[];
130
+ endings?: Ending[];
131
+ settings?: FormSettings;
132
+ meta?: Record<string, Json>;
133
+ }
134
+ interface Form<A = Record<string, unknown>, H = Record<string, unknown>> {
135
+ readonly definition: FormDefinition;
136
+ /** The canonical JSON, identical to what a builder would produce. */
137
+ toJSON(): FormDefinition;
138
+ /** Content fingerprint of the canonical JSON. */
139
+ hash(): string;
140
+ /** Type-level only: the answers, keyed by field key. */
141
+ readonly $answers: A;
142
+ /** Type-level only: the hidden inputs. */
143
+ readonly $hidden: H;
144
+ }
145
+ /**
146
+ * Define a form in code. The result's `definition` is the same canonical JSON
147
+ * a builder or `parseForm` produces; answer types are inferred from the blocks.
148
+ */
149
+ declare function defineForm<const B extends readonly AnyBlock[], H extends Record<string, HiddenSpec> = Record<never, HiddenSpec>>(input: FormInput<B, H>): Form<AnswersOf<B>, {
150
+ [K in keyof H]?: HiddenValue<H[K]["type"]>;
151
+ }>;
152
+
153
+ interface FormIssue {
154
+ path: string;
155
+ message: string;
156
+ }
157
+ type ParseResult = {
158
+ ok: true;
159
+ form: FormDefinition;
160
+ warnings: FormIssue[];
161
+ } | {
162
+ ok: false;
163
+ issues: FormIssue[];
164
+ warnings: FormIssue[];
165
+ };
166
+ /**
167
+ * Check a definition before it is stored or rendered: structure, the types of
168
+ * text fields, unique ids and keys, known field types, references that
169
+ * resolve, operators that fit the field type. Warnings flag things that work
170
+ * but are probably mistakes. Never throws, whatever the input.
171
+ */
172
+ declare function parseForm(input: unknown): ParseResult;
173
+
174
+ /** Longest string a hidden input accepts. Longer values are dropped. */
175
+ declare const MAX_HIDDEN_STRING_LENGTH = 1000;
176
+ interface CoerceHiddenOptions {
177
+ /** Coerce only inputs with these sources; the rest are reported in `ignored`. Default: all. */
178
+ sources?: readonly HiddenInput["source"][];
179
+ }
180
+ interface CoercedHidden {
181
+ /** Declared inputs, typed. */
182
+ values: Record<string, Json>;
183
+ /** Names dropped: undeclared, from another source, or not convertible to the declared type. */
184
+ ignored: string[];
185
+ }
186
+ /**
187
+ * Convert one raw value to a hidden input's declared type, or `undefined` when
188
+ * it cannot be. Only string, number and boolean primitives are accepted.
189
+ * - `string`: a string (at most 1000 characters), or a number or boolean as text.
190
+ * - `number`: a finite number, or a non-blank numeric string. `""` is not a number.
191
+ * - `boolean`: `true`/`false`, or the strings `"true"`/`"false"`.
192
+ */
193
+ declare function coerceHiddenValue(value: unknown, type: HiddenInput["type"]): Json | undefined;
194
+ /**
195
+ * Type raw hidden inputs (from a query string, the host app or the server)
196
+ * by the definition's declarations. `undefined` values are skipped silently.
197
+ */
198
+ declare function coerceHidden(definition: FormDefinition, raw: Record<string, unknown> | undefined, options?: CoerceHiddenOptions): CoercedHidden;
199
+
200
+ interface EvalScope {
201
+ values: Record<string, Json | undefined>;
202
+ hidden: Record<string, Json | undefined>;
203
+ fields: Map<string, FieldBlock>;
204
+ }
205
+ /**
206
+ * Evaluate a condition. Never throws. An empty value (unanswered, `null`,
207
+ * blank text, an empty list: see `isEmptyValue`) is unanswered: it fails
208
+ * every comparison, including `eq`, `in` and `contains`, and passes `neq`.
209
+ */
210
+ declare function evaluate(condition: Condition, scope: EvalScope): boolean;
211
+ /** Blocks a respondent steps through: fields and content, in order. */
212
+ declare function steps(def: FormDefinition): Block[];
213
+ interface PathResult {
214
+ /** Block ids the respondent will step through, given the answers on the path. */
215
+ path: string[];
216
+ /** The ending reached by a jump, or the default ending. */
217
+ ending: string | null;
218
+ }
219
+ /**
220
+ * The respondent's path, derived from the answers on the path, so the client,
221
+ * the server and a replay always agree. It is built in one forward pass: a
222
+ * block's show/hide rules, and the jumps from it, see only the fields already
223
+ * on the path before it (and, for a jump, the block itself). A field that is
224
+ * hidden, jumped over or later in the form reads as unanswered, whatever
225
+ * value it holds.
226
+ *
227
+ * A block jumped over is off the path and counts as hidden: it is not
228
+ * validated and its value is cleared on submit. A jump never revisits a block;
229
+ * a jump to an earlier block is ignored (the checker in `parseForm` reports it).
230
+ */
231
+ declare function computePath(def: FormDefinition, scope: EvalScope): PathResult;
232
+ /**
233
+ * Whether a block's show/hide rules let it be shown, ignoring jumps. The rules
234
+ * read the answers on the path before the block, as in `computePath`. A block
235
+ * with any `show` rule is hidden until one matches; a matching `hide` rule
236
+ * always wins.
237
+ */
238
+ declare function ruleVisible(def: FormDefinition, blockId: string, scope: EvalScope): boolean;
239
+
240
+ /**
241
+ * A field type, without any UI. Renderers are registered separately per
242
+ * platform (`genesis-forms-react`), so a manifest runs anywhere.
243
+ */
244
+ interface FieldTypeManifest {
245
+ type: string;
246
+ label: string;
247
+ category: "text" | "choice" | "scale" | "date" | "media" | "advanced";
248
+ /** `json: false` types must convert, so drafts and server replay never lose an answer. */
249
+ value: {
250
+ json: true;
251
+ } | {
252
+ json: false;
253
+ serialize: (value: unknown) => Json;
254
+ deserialize: (json: Json) => unknown;
255
+ };
256
+ operators: Operator[];
257
+ a11y: {
258
+ semantics: "control" | "group";
259
+ /** The HTML `autocomplete` token, such as `"email"` or `"tel"`. */
260
+ autocomplete?: string;
261
+ /** The HTML `inputmode` hint for on-screen keyboards, such as `"email"` or `"tel"`. */
262
+ inputMode?: "text" | "email" | "tel" | "numeric" | "decimal" | "url" | "search";
263
+ };
264
+ isEmpty?: (value: Json | undefined) => boolean;
265
+ /** Return an error message, or null when the (non-empty) value is valid. */
266
+ validate?: (value: Json, field: FieldBlock) => string | null;
267
+ /** Plain text for recall, exports and emails. */
268
+ summarize?: (value: Json, field: FieldBlock) => string;
269
+ /**
270
+ * The form a (valid, non-empty) value is stored in, such as an email
271
+ * without surrounding spaces. Applied by `engine.answers()` and the server.
272
+ */
273
+ normalize?: (value: Json, field: FieldBlock) => Json;
274
+ /** Valid example answers, used by `coverage()` and other test tools to explore a path. */
275
+ samples?: (field: FieldBlock) => Json[];
276
+ }
277
+ declare function registerFieldType(manifest: FieldTypeManifest): void;
278
+ declare function getFieldType(type: string): FieldTypeManifest | undefined;
279
+ declare function listFieldTypes(): FieldTypeManifest[];
280
+ declare function isEmptyValue(value: Json | undefined, field?: FieldBlock): boolean;
281
+ /** A value in its stored form (see `FieldTypeManifest.normalize`). */
282
+ declare function normalizeValue(value: Json, field: FieldBlock): Json;
283
+ /** Whether a block holds personal or secret data: `sensitive: true`, or the `props.sensitive: true` alias. */
284
+ declare function isSensitive(block: Block): boolean;
285
+ declare function summarizeValue(value: Json | undefined, field: FieldBlock): string;
286
+ interface ChoiceOption {
287
+ value: string;
288
+ label?: string;
289
+ }
290
+ declare function choiceOptions(field: FieldBlock): ChoiceOption[];
291
+
292
+ interface RecallScope {
293
+ /** Fields by key. */
294
+ fieldsByKey: Map<string, FieldBlock>;
295
+ values: Record<string, Json | undefined>;
296
+ hidden: Record<string, Json | undefined>;
297
+ }
298
+ /**
299
+ * Replace `{{field:key|fallback}}` and `{{hidden:name|fallback}}` tokens.
300
+ * An unanswered value renders its fallback (default empty); the text is
301
+ * never dropped.
302
+ */
303
+ declare function recall(text: string, scope: RecallScope): string;
304
+
305
+ /** JSON with object keys sorted, so equal definitions serialize identically. */
306
+ declare function canonicalJson(value: unknown): string;
307
+ /**
308
+ * A short content fingerprint (cyrb53, 14 hex characters). It detects drift
309
+ * between a definition and its published version; it is not a security hash.
310
+ */
311
+ declare function fingerprint(value: Json | object): string;
312
+
313
+ /**
314
+ * "Why is this hidden?" A structured trace of one block's visibility, for
315
+ * devtools and tests. Pure: it reads a definition and values, never an engine's
316
+ * internals, so it explains a stored response as well as a live one.
317
+ */
318
+
319
+ interface ExplainInput {
320
+ definition: FormDefinition;
321
+ values: Record<string, Json | undefined>;
322
+ hidden?: Record<string, Json | undefined>;
323
+ }
324
+ interface RuleTrace {
325
+ ruleId: string;
326
+ effect: "show" | "hide";
327
+ /** The rule's condition, evaluated against the current values. */
328
+ matched: boolean;
329
+ /** "Hidden because contact_me is not Yes." */
330
+ sentence: string;
331
+ }
332
+ interface JumpTrace {
333
+ ruleId: string;
334
+ /** Block id the jump fired from. */
335
+ from: string;
336
+ to: string | {
337
+ ending: string;
338
+ };
339
+ sentence: string;
340
+ }
341
+ interface FieldValidation {
342
+ /** `not-checked`: the field is hidden, so it is neither validated nor stored. */
343
+ status: "valid" | "invalid" | "unanswered" | "not-checked";
344
+ message?: string;
345
+ }
346
+ interface Explanation {
347
+ blockId: string;
348
+ /** The field key, or the block id for content. */
349
+ key: string;
350
+ kind: "field" | "content" | "page-break";
351
+ /** On the respondent's path given the answers so far. */
352
+ onPath: boolean;
353
+ /** Visible by show/hide rules alone (ignoring jumps). */
354
+ ruleVisible: boolean;
355
+ /** What the respondent sees: on the path and rule-visible. */
356
+ visible: boolean;
357
+ /** The jump that carried the path past this block, if any. */
358
+ skippedBy?: JumpTrace;
359
+ /** Every show/hide rule targeting this block, in definition order. */
360
+ rules: RuleTrace[];
361
+ /** Fields only. */
362
+ validation?: FieldValidation;
363
+ /** One plain-English line: the main reason. */
364
+ summary: string;
365
+ }
366
+ interface ExplainOptions {
367
+ validators?: Record<string, FieldValidator>;
368
+ }
369
+ /** Explain a block's visibility. `blockId` may also be a field key. */
370
+ declare function explain(source: FormEngine | ExplainInput, blockId: string, options?: ExplainOptions): Explanation;
371
+ /** A condition as a plain-English clause: `contact_me is not Yes`. */
372
+ declare function describeCondition(condition: Condition, def: FormDefinition, fields?: Map<string, FieldBlock>): string;
373
+
374
+ export { type AnswersOf, Block, type ChoiceFieldOptions, type ChoiceOption, type CoerceHiddenOptions, type CoercedHidden, Condition, ContentBlock, Ending, type ExplainInput, type ExplainOptions, type Explanation, FieldBlock, type FieldOptions, type FieldSpec, type FieldTypeManifest, type FieldValidation, FieldValidator, type Form, FormDefinition, FormEngine, type FormInput, type FormIssue, FormLayout, FormSettings, HiddenInput, Json, type JumpTrace, type KeyResolver, LogicRule, MAX_HIDDEN_STRING_LENGTH, Operator, PageBreakBlock, type ParseResult, type PathResult, type RuleSpec, type RuleTrace, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, defineForm, describeCondition, evaluate, explain, f, fingerprint, getFieldType, isEmptyValue, isSensitive, listFieldTypes, normalizeValue, parseForm, recall, registerFieldType, ruleVisible, steps, summarizeValue, when };