@marktiderman/genesis-forms 0.2.0 → 0.3.1

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 +1235 -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 +1193 -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 +322 -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 +311 -25
  25. package/dist/index.js.map +1 -1
  26. package/dist/server.cjs +249 -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 +249 -22
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.cjs +253 -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 +253 -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/server.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) {
@@ -610,7 +792,7 @@ function shown(v) {
610
792
  text ??= typeof v === "bigint" ? `${v}n` : typeof v;
611
793
  return text.length > 60 ? `${text.slice(0, 57)}...` : text;
612
794
  }
613
- var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
795
+ var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
614
796
  var isInt = (v) => typeof v === "number" && Number.isInteger(v);
615
797
  var LITERAL_TYPE = {
616
798
  "short-text": "string",
@@ -621,7 +803,8 @@ var LITERAL_TYPE = {
621
803
  nps: "number",
622
804
  "single-choice": "string",
623
805
  "multi-choice": "string",
624
- "yes-no": "boolean"
806
+ "yes-no": "boolean",
807
+ list: "string"
625
808
  };
626
809
  var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
627
810
  var OPERATORS = /* @__PURE__ */ new Set([
@@ -663,7 +846,48 @@ function parseForm(input) {
663
846
  }
664
847
  return true;
665
848
  };
666
- if (!isObject(input)) {
849
+ const optionalInt = (v, path, floor) => {
850
+ if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
851
+ };
852
+ const ordered = (low, high, path, what) => {
853
+ if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
854
+ };
855
+ const structuredProps = (type, props, path) => {
856
+ if (type === "list") {
857
+ optionalInt(props.min, `${path}.min`, 0);
858
+ optionalInt(props.max, `${path}.max`, 1);
859
+ optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
860
+ optionalText(props.placeholder, `${path}.placeholder`);
861
+ ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
862
+ } else if (type === "table") {
863
+ const columns = props.columns;
864
+ if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
865
+ issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
866
+ } else {
867
+ const seen = /* @__PURE__ */ new Set();
868
+ columns.forEach((c, j) => {
869
+ const at = `${path}.columns[${j}]`;
870
+ if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
871
+ if (name(c.key, `${at}.key`, "A column key")) {
872
+ if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
873
+ seen.add(c.key);
874
+ }
875
+ text(c.label, `${at}.label`);
876
+ optionalBoolean(c.long, `${at}.long`);
877
+ });
878
+ }
879
+ optionalInt(props.minRows, `${path}.minRows`, 0);
880
+ optionalInt(props.maxRows, `${path}.maxRows`, 1);
881
+ ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
882
+ } else if (type === "media-notes") {
883
+ optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
884
+ optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
885
+ } else if (type === "text-highlights") {
886
+ optionalText(props.text, `${path}.text`);
887
+ optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
888
+ }
889
+ };
890
+ if (!isObject2(input)) {
667
891
  issue("", "A form definition must be an object.");
668
892
  return fail();
669
893
  }
@@ -672,19 +896,19 @@ function parseForm(input) {
672
896
  text(input.title, "title");
673
897
  optionalText(input.description, "description");
674
898
  if (input.layout !== void 0) {
675
- if (!isObject(input.layout)) issue("layout", "Must be an object.");
899
+ if (!isObject2(input.layout)) issue("layout", "Must be an object.");
676
900
  else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
677
901
  issue("layout.mode", "Must be focus or classic.");
678
902
  }
679
903
  }
680
904
  if (input.settings !== void 0) {
681
- if (!isObject(input.settings)) issue("settings", "Must be an object.");
905
+ if (!isObject2(input.settings)) issue("settings", "Must be an object.");
682
906
  else {
683
907
  optionalBoolean(input.settings.anonymous, "settings.anonymous");
684
908
  optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
685
909
  }
686
910
  }
687
- if (input.meta !== void 0 && (!isObject(input.meta) || !isJson(input.meta))) {
911
+ if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
688
912
  issue("meta", "Must be a JSON object.");
689
913
  }
690
914
  if (!Array.isArray(input.blocks)) {
@@ -699,7 +923,7 @@ function parseForm(input) {
699
923
  const choices = /* @__PURE__ */ new Map();
700
924
  input.blocks.forEach((block, i) => {
701
925
  const path = `blocks[${i}]`;
702
- if (!isObject(block) || typeof block.id !== "string" || !block.id) {
926
+ if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
703
927
  issue(path, "Every block needs a string id.");
704
928
  return;
705
929
  }
@@ -720,23 +944,23 @@ function parseForm(input) {
720
944
  issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
721
945
  }
722
946
  if (block.props !== void 0) {
723
- if (!isObject(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
947
+ if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
724
948
  else if (block.props.options !== void 0) {
725
949
  const options = block.props.options;
726
950
  if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
727
951
  else {
728
952
  options.forEach((o, j) => {
729
953
  const at = `${path}.props.options[${j}]`;
730
- if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
954
+ if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
731
955
  else optionalText(o.label, `${at}.label`);
732
956
  });
733
957
  choices.set(
734
958
  block.id,
735
- new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
959
+ new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
736
960
  );
737
961
  }
738
962
  }
739
- const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
963
+ const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
740
964
  if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
741
965
  issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
742
966
  }
@@ -744,6 +968,9 @@ function parseForm(input) {
744
968
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
745
969
  }
746
970
  }
971
+ if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
972
+ structuredProps(block.type, block.props ?? {}, `${path}.props`);
973
+ }
747
974
  if (validKey) {
748
975
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
749
976
  keys.set(block.key, block.id);
@@ -762,7 +989,7 @@ function parseForm(input) {
762
989
  const hiddenTypes = /* @__PURE__ */ new Map();
763
990
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
764
991
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
765
- if (!isObject(h) || typeof h.name !== "string" || !h.name) {
992
+ if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
766
993
  issue(`hidden[${i}]`, "Each hidden input needs a name.");
767
994
  return;
768
995
  }
@@ -783,7 +1010,7 @@ function parseForm(input) {
783
1010
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
784
1011
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
785
1012
  const path = `endings[${i}]`;
786
- if (!isObject(e)) {
1013
+ if (!isObject2(e)) {
787
1014
  issue(path, "Each ending must be an object with an id and a title.");
788
1015
  return;
789
1016
  }
@@ -795,7 +1022,7 @@ function parseForm(input) {
795
1022
  });
796
1023
  const checkOperand = (o, path, reads) => {
797
1024
  if (o === void 0) return;
798
- if (!isObject(o)) return issue(path, "An operand must be an object.");
1025
+ if (!isObject2(o)) return issue(path, "An operand must be an object.");
799
1026
  if ("field" in o) {
800
1027
  if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
801
1028
  reads.push(o.field);
@@ -808,7 +1035,7 @@ function parseForm(input) {
808
1035
  }
809
1036
  };
810
1037
  const checkCondition = (c, path, reads, depth = 0) => {
811
- if (!isObject(c)) return issue(path, "A condition must be an object.");
1038
+ if (!isObject2(c)) return issue(path, "A condition must be an object.");
812
1039
  if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
813
1040
  if ("all" in c || "any" in c) {
814
1041
  const group = "all" in c ? "all" : "any";
@@ -817,7 +1044,7 @@ function parseForm(input) {
817
1044
  return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
818
1045
  }
819
1046
  if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
820
- if (typeof c.op !== "string" || !isObject(c.left)) {
1047
+ if (typeof c.op !== "string" || !isObject2(c.left)) {
821
1048
  return issue(path, "A comparison needs an `op` and a `left` operand.");
822
1049
  }
823
1050
  if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
@@ -833,7 +1060,7 @@ function parseForm(input) {
833
1060
  };
834
1061
  const checkLiterals = (op, left, right, path) => {
835
1062
  if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
836
- const literal = isObject(right) && "value" in right;
1063
+ const literal = isObject2(right) && "value" in right;
837
1064
  if (right !== void 0 && !literal) return;
838
1065
  let what = "the left side";
839
1066
  let type;
@@ -872,7 +1099,7 @@ function parseForm(input) {
872
1099
  if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
873
1100
  (Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
874
1101
  const path = `logic[${i}]`;
875
- if (!isObject(rule) || typeof rule.id !== "string" || !isObject(rule.when)) {
1102
+ if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
876
1103
  issue(path, "Each rule needs an id and a `when` condition.");
877
1104
  return;
878
1105
  }
@@ -891,7 +1118,7 @@ function parseForm(input) {
891
1118
  at = rule.from;
892
1119
  inclusive = true;
893
1120
  }
894
- if (isObject(rule.to)) {
1121
+ if (isObject2(rule.to)) {
895
1122
  if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
896
1123
  issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
897
1124
  }
@@ -923,7 +1150,7 @@ function parseForm(input) {
923
1150
  }
924
1151
  });
925
1152
  input.blocks.forEach((block, i) => {
926
- if (!isObject(block)) return;
1153
+ if (!isObject2(block)) return;
927
1154
  const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
928
1155
  const texts = raw.filter((t) => typeof t === "string");
929
1156
  for (const t of texts) {
@@ -943,7 +1170,7 @@ function parseForm(input) {
943
1170
  });
944
1171
  const extra = [["description", input.description]];
945
1172
  (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
946
- if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1173
+ if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
947
1174
  });
948
1175
  for (const [path, t] of extra) {
949
1176
  if (typeof t !== "string") continue;