@marktiderman/genesis-forms 0.2.0 → 0.3.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 (42) hide show
  1. package/README.md +113 -13
  2. package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
  3. package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
  4. package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
  5. package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
  6. package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
  7. package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
  8. package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
  9. package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
  10. package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
  11. package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
  12. package/dist/destinations.cjs +1211 -0
  13. package/dist/destinations.cjs.map +1 -0
  14. package/dist/destinations.d.cts +465 -0
  15. package/dist/destinations.d.ts +465 -0
  16. package/dist/destinations.js +1169 -0
  17. package/dist/destinations.js.map +1 -0
  18. package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
  19. package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
  20. package/dist/index.cjs +320 -24
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +102 -5
  23. package/dist/index.d.ts +102 -5
  24. package/dist/index.js +309 -25
  25. package/dist/index.js.map +1 -1
  26. package/dist/server.cjs +247 -22
  27. package/dist/server.cjs.map +1 -1
  28. package/dist/server.d.cts +3 -2
  29. package/dist/server.d.ts +3 -2
  30. package/dist/server.js +247 -22
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.cjs +251 -22
  33. package/dist/testing.cjs.map +1 -1
  34. package/dist/testing.d.cts +4 -3
  35. package/dist/testing.d.ts +4 -3
  36. package/dist/testing.js +251 -22
  37. package/dist/testing.js.map +1 -1
  38. package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
  39. package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
  40. package/dist/validate-BXXqi0fq.d.cts +30 -0
  41. package/dist/validate-BjNwF1UE.d.ts +30 -0
  42. package/package.json +14 -2
package/dist/testing.cjs CHANGED
@@ -1,5 +1,187 @@
1
1
  'use strict';
2
2
 
3
+ // src/structured.ts
4
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
5
+ function intProp(field, name, fallback, floor = 1) {
6
+ const v = field.props?.[name];
7
+ return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
8
+ }
9
+ function optionalIntProp(field, name, floor = 0) {
10
+ const v = field.props?.[name];
11
+ return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
12
+ }
13
+ var blank = (v) => typeof v !== "string" || v.trim() === "";
14
+ var blankText = (v) => typeof v === "string" && v.trim() === "";
15
+ var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
16
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
17
+ function listLimits(field) {
18
+ return { min: optionalIntProp(field, "min") ?? 0, max: intProp(field, "max", 10), itemMaxLength: intProp(field, "itemMaxLength", 500) };
19
+ }
20
+ var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
21
+ var MAX_TABLE_COLUMNS = 6;
22
+ var MAX_TABLE_CELL_LENGTH = 2e3;
23
+ function tableColumns(field) {
24
+ const raw = field.props?.columns;
25
+ if (!Array.isArray(raw)) return [];
26
+ const seen = /* @__PURE__ */ new Set();
27
+ const out = [];
28
+ for (const c of raw) {
29
+ if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
30
+ seen.add(c.key);
31
+ const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
32
+ if (c.long === true) column.long = true;
33
+ out.push(column);
34
+ }
35
+ return out;
36
+ }
37
+ function tableLimits(field) {
38
+ return { minRows: optionalIntProp(field, "minRows") ?? 0, maxRows: intProp(field, "maxRows", 3) };
39
+ }
40
+ var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
41
+ var MAX_TIMESTAMP_SECONDS = 86400;
42
+ function formatTimestamp(seconds) {
43
+ const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
44
+ const h = Math.floor(total / 3600);
45
+ const m = Math.floor(total % 3600 / 60);
46
+ const s = String(total % 60).padStart(2, "0");
47
+ return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
48
+ }
49
+ var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
50
+ var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
51
+ var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
52
+ var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
53
+ var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
54
+ var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
55
+ var structuredFieldTypes = [
56
+ {
57
+ type: "list",
58
+ label: "List",
59
+ category: "text",
60
+ value: { json: true },
61
+ // logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
62
+ operators: ["contains", "answered", "empty"],
63
+ a11y: { semantics: "group" },
64
+ isEmpty: (value) => emptyList(value, blankText),
65
+ validate: (value, field) => {
66
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
67
+ const { min, max, itemMaxLength } = listLimits(field);
68
+ if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
69
+ const count = listItems(value).length;
70
+ if (count < min) return `Add at least ${plural(min, "item")}.`;
71
+ if (count > max) return `Add at most ${plural(max, "item")}.`;
72
+ return null;
73
+ },
74
+ normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
75
+ summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
76
+ samples: (field) => {
77
+ const { min, max } = listLimits(field);
78
+ return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
79
+ }
80
+ },
81
+ {
82
+ type: "table",
83
+ label: "Table",
84
+ category: "advanced",
85
+ value: { json: true },
86
+ operators: ["answered", "empty"],
87
+ a11y: { semantics: "group" },
88
+ isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
89
+ validate: (value, field) => {
90
+ const columns = tableColumns(field);
91
+ if (!columns.length) return "This question has no columns.";
92
+ if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
93
+ const keys = new Set(columns.map((c) => c.key));
94
+ for (const row of value) {
95
+ for (const [key, cell] of Object.entries(row)) {
96
+ if (!keys.has(key)) return "An answer names a column this question doesn't have.";
97
+ if (typeof cell !== "string") return "Fill in the rows with text.";
98
+ if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
99
+ }
100
+ }
101
+ const { minRows, maxRows } = tableLimits(field);
102
+ const count = filledRows(value).length;
103
+ if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
104
+ if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
105
+ return null;
106
+ },
107
+ normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
108
+ const out = {};
109
+ for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
110
+ return out;
111
+ }) : value,
112
+ summarize: (value, field) => {
113
+ const columns = tableColumns(field);
114
+ return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
115
+ },
116
+ samples: (field) => {
117
+ const columns = tableColumns(field);
118
+ if (!columns.length) return [];
119
+ const { minRows, maxRows } = tableLimits(field);
120
+ const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
121
+ return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
122
+ }
123
+ },
124
+ {
125
+ type: "media-notes",
126
+ label: "Notes at moments",
127
+ category: "media",
128
+ value: { json: true },
129
+ operators: ["answered", "empty"],
130
+ a11y: { semantics: "group" },
131
+ isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
132
+ validate: (value, field) => {
133
+ if (!Array.isArray(value)) return "Add notes.";
134
+ const textMaxLength = intProp(field, "textMaxLength", 1e3);
135
+ for (const note of value) {
136
+ if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
137
+ if (!isTimestamp(note.at)) return "Each note needs a time.";
138
+ if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
139
+ }
140
+ const maxNotes = intProp(field, "maxNotes", 20);
141
+ return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
142
+ },
143
+ normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
144
+ summarize: (value) => notesWithText(value).map((n) => ({ at: typeof n.at === "number" ? n.at : 0, text: n.text.trim() })).sort((a, b) => a.at - b.at).map((n) => `${formatTimestamp(n.at)} ${n.text}`).join("; "),
145
+ samples: () => [[{ at: 0, text: "sample" }]]
146
+ },
147
+ {
148
+ type: "text-highlights",
149
+ label: "Highlights",
150
+ category: "media",
151
+ value: { json: true },
152
+ operators: ["answered", "empty"],
153
+ a11y: { semantics: "group" },
154
+ isEmpty: (value) => emptyList(value, () => false),
155
+ validate: (value, field) => {
156
+ if (!Array.isArray(value)) return "Highlight a passage.";
157
+ const maxHighlights = intProp(field, "maxHighlights", 20);
158
+ if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
159
+ const text = sourceText(field);
160
+ for (const h of value) {
161
+ if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
162
+ return "Highlight a passage of the text.";
163
+ }
164
+ const start = h.start;
165
+ const end = h.end;
166
+ if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
167
+ if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
168
+ if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
169
+ if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
170
+ if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
171
+ }
172
+ return null;
173
+ },
174
+ normalize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => ({ start: h.start, end: h.end, quote: h.quote, note: String(h.note).trim() })).sort((a, b) => a.start - b.start || a.end - b.end) : value,
175
+ summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
176
+ samples: (field) => {
177
+ const text = sourceText(field);
178
+ if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
179
+ const end = Math.min(text.length, 20);
180
+ return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
181
+ }
182
+ }
183
+ ];
184
+
3
185
  // src/fields.ts
4
186
  var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
5
187
  var globalScope = globalThis;
@@ -181,7 +363,7 @@ var builtins = [
181
363
  summarize: (value) => value ? "Yes" : "No"
182
364
  }
183
365
  ];
184
- for (const manifest of builtins) registerFieldType(manifest);
366
+ for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
185
367
 
186
368
  // src/events.ts
187
369
  function isPlainObject(value) {
@@ -670,6 +852,10 @@ function domainOf(field, extra, maxOptions) {
670
852
  extra = extra.map((v) => typeof v === "string" ? [v] : v);
671
853
  break;
672
854
  }
855
+ case "list":
856
+ base = getFieldType(field.type)?.samples?.(field) ?? [];
857
+ extra = extra.map((v) => typeof v === "string" ? [v] : v);
858
+ break;
673
859
  case "email":
674
860
  base = ["ada@example.com"];
675
861
  break;
@@ -783,7 +969,7 @@ function shown(v) {
783
969
  text ??= typeof v === "bigint" ? `${v}n` : typeof v;
784
970
  return text.length > 60 ? `${text.slice(0, 57)}...` : text;
785
971
  }
786
- var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
972
+ var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
787
973
  var isInt = (v) => typeof v === "number" && Number.isInteger(v);
788
974
  var LITERAL_TYPE = {
789
975
  "short-text": "string",
@@ -794,7 +980,8 @@ var LITERAL_TYPE = {
794
980
  nps: "number",
795
981
  "single-choice": "string",
796
982
  "multi-choice": "string",
797
- "yes-no": "boolean"
983
+ "yes-no": "boolean",
984
+ list: "string"
798
985
  };
799
986
  var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
800
987
  var OPERATORS = /* @__PURE__ */ new Set([
@@ -836,7 +1023,48 @@ function parseForm(input) {
836
1023
  }
837
1024
  return true;
838
1025
  };
839
- if (!isObject(input)) {
1026
+ const optionalInt = (v, path, floor) => {
1027
+ if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
1028
+ };
1029
+ const ordered = (low, high, path, what) => {
1030
+ if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
1031
+ };
1032
+ const structuredProps = (type, props, path) => {
1033
+ if (type === "list") {
1034
+ optionalInt(props.min, `${path}.min`, 0);
1035
+ optionalInt(props.max, `${path}.max`, 1);
1036
+ optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
1037
+ optionalText(props.placeholder, `${path}.placeholder`);
1038
+ ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
1039
+ } else if (type === "table") {
1040
+ const columns = props.columns;
1041
+ if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
1042
+ issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
1043
+ } else {
1044
+ const seen = /* @__PURE__ */ new Set();
1045
+ columns.forEach((c, j) => {
1046
+ const at = `${path}.columns[${j}]`;
1047
+ if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
1048
+ if (name(c.key, `${at}.key`, "A column key")) {
1049
+ if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
1050
+ seen.add(c.key);
1051
+ }
1052
+ text(c.label, `${at}.label`);
1053
+ optionalBoolean(c.long, `${at}.long`);
1054
+ });
1055
+ }
1056
+ optionalInt(props.minRows, `${path}.minRows`, 0);
1057
+ optionalInt(props.maxRows, `${path}.maxRows`, 1);
1058
+ ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
1059
+ } else if (type === "media-notes") {
1060
+ optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
1061
+ optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
1062
+ } else if (type === "text-highlights") {
1063
+ optionalText(props.text, `${path}.text`);
1064
+ optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
1065
+ }
1066
+ };
1067
+ if (!isObject2(input)) {
840
1068
  issue("", "A form definition must be an object.");
841
1069
  return fail();
842
1070
  }
@@ -845,19 +1073,19 @@ function parseForm(input) {
845
1073
  text(input.title, "title");
846
1074
  optionalText(input.description, "description");
847
1075
  if (input.layout !== void 0) {
848
- if (!isObject(input.layout)) issue("layout", "Must be an object.");
1076
+ if (!isObject2(input.layout)) issue("layout", "Must be an object.");
849
1077
  else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
850
1078
  issue("layout.mode", "Must be focus or classic.");
851
1079
  }
852
1080
  }
853
1081
  if (input.settings !== void 0) {
854
- if (!isObject(input.settings)) issue("settings", "Must be an object.");
1082
+ if (!isObject2(input.settings)) issue("settings", "Must be an object.");
855
1083
  else {
856
1084
  optionalBoolean(input.settings.anonymous, "settings.anonymous");
857
1085
  optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
858
1086
  }
859
1087
  }
860
- if (input.meta !== void 0 && (!isObject(input.meta) || !isJson(input.meta))) {
1088
+ if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
861
1089
  issue("meta", "Must be a JSON object.");
862
1090
  }
863
1091
  if (!Array.isArray(input.blocks)) {
@@ -872,7 +1100,7 @@ function parseForm(input) {
872
1100
  const choices = /* @__PURE__ */ new Map();
873
1101
  input.blocks.forEach((block, i) => {
874
1102
  const path = `blocks[${i}]`;
875
- if (!isObject(block) || typeof block.id !== "string" || !block.id) {
1103
+ if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
876
1104
  issue(path, "Every block needs a string id.");
877
1105
  return;
878
1106
  }
@@ -893,29 +1121,30 @@ function parseForm(input) {
893
1121
  issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
894
1122
  }
895
1123
  if (block.props !== void 0) {
896
- if (!isObject(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
1124
+ if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
897
1125
  else if (block.props.options !== void 0) {
898
1126
  const options = block.props.options;
899
1127
  if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
900
1128
  else {
901
1129
  options.forEach((o, j) => {
902
1130
  const at = `${path}.props.options[${j}]`;
903
- if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
1131
+ if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
904
1132
  else optionalText(o.label, `${at}.label`);
905
1133
  });
906
1134
  choices.set(
907
1135
  block.id,
908
- new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
1136
+ new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
909
1137
  );
910
1138
  }
911
1139
  }
912
- const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
1140
+ const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
913
1141
  if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
914
1142
  issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
915
1143
  }
916
1144
  if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
917
1145
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
918
1146
  }
1147
+ if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
919
1148
  }
920
1149
  if (validKey) {
921
1150
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -935,7 +1164,7 @@ function parseForm(input) {
935
1164
  const hiddenTypes = /* @__PURE__ */ new Map();
936
1165
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
937
1166
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
938
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
1167
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
939
1168
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
940
1169
  return;
941
1170
  }
@@ -956,7 +1185,7 @@ function parseForm(input) {
956
1185
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
957
1186
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
958
1187
  const path = `endings[${i}]`;
959
- if (!isObject(e)) {
1188
+ if (!isObject2(e)) {
960
1189
  issue(path, "Each ending must be an object with an id and a title.");
961
1190
  return;
962
1191
  }
@@ -968,7 +1197,7 @@ function parseForm(input) {
968
1197
  });
969
1198
  const checkOperand = (o, path, reads) => {
970
1199
  if (o === void 0) return;
971
- if (!isObject(o)) return issue(path, "An operand must be an object.");
1200
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
972
1201
  if ("field" in o) {
973
1202
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
974
1203
  reads.push(o.field);
@@ -981,7 +1210,7 @@ function parseForm(input) {
981
1210
  }
982
1211
  };
983
1212
  const checkCondition = (c, path, reads, depth = 0) => {
984
- if (!isObject(c)) return issue(path, "A condition must be an object.");
1213
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
985
1214
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
986
1215
  if ("all" in c || "any" in c) {
987
1216
  const group = "all" in c ? "all" : "any";
@@ -990,7 +1219,7 @@ function parseForm(input) {
990
1219
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
991
1220
  }
992
1221
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
993
- if (typeof c.op !== "string" || !isObject(c.left)) {
1222
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
994
1223
  return issue(path, "A comparison needs an `op` and a `left` operand.");
995
1224
  }
996
1225
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -1006,7 +1235,7 @@ function parseForm(input) {
1006
1235
  };
1007
1236
  const checkLiterals = (op, left, right, path) => {
1008
1237
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
1009
- const literal = isObject(right) && "value" in right;
1238
+ const literal = isObject2(right) && "value" in right;
1010
1239
  if (right !== void 0 && !literal) return;
1011
1240
  let what = "the left side";
1012
1241
  let type;
@@ -1045,7 +1274,7 @@ function parseForm(input) {
1045
1274
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
1046
1275
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
1047
1276
  const path = `logic[${i}]`;
1048
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
1277
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
1049
1278
  issue(path, "Each rule needs an id and a `when` condition.");
1050
1279
  return;
1051
1280
  }
@@ -1064,7 +1293,7 @@ function parseForm(input) {
1064
1293
  at = rule.from;
1065
1294
  inclusive = true;
1066
1295
  }
1067
- if (isObject(rule.to)) {
1296
+ if (isObject2(rule.to)) {
1068
1297
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
1069
1298
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
1070
1299
  }
@@ -1096,7 +1325,7 @@ function parseForm(input) {
1096
1325
  }
1097
1326
  });
1098
1327
  input.blocks.forEach((block, i) => {
1099
- if (!isObject(block)) return;
1328
+ if (!isObject2(block)) return;
1100
1329
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
1101
1330
  const texts = raw.filter((t) => typeof t === "string");
1102
1331
  for (const t of texts) {
@@ -1116,7 +1345,7 @@ function parseForm(input) {
1116
1345
  });
1117
1346
  const extra = [["description", input.description]];
1118
1347
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
1119
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1348
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1120
1349
  });
1121
1350
  for (const [path, t] of extra) {
1122
1351
  if (typeof t !== "string") continue;